﻿// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved
// Developer:  Michael Antonio
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using MichMan.Utilities;
using System.Runtime.Serialization;

namespace MichMan.Utilities.Requests
{
    [DataContract]
    public class CachedItem
    {
        public CachedItem(object item, DateTime? expires)
        {
            Expires = expires;
            Item = item;
            LastAccessed = DateTime.UtcNow;
        }

        [DataMember]
        public string Data
        {
            get
            {
                if (Item is string)
                {
                    return Item as string;
                }
                return null;
            }
            set
            {
                Item = value;
            }
        }

        [IgnoreDataMember]
        public object Item { get; private set; }
        [DataMember]
        public DateTime? Expires { get; set; }
        [DataMember]
        public DateTime LastAccessed { get; set; }
    }

    /// <summary>
    /// Cache implemented as a dictionary with expiration.
    /// </summary>
    public class DictionaryCache : SimpleDictionaryCache
    {
        public DictionaryCache()
        {
            CacheLimit = long.MaxValue;
        }

        /// <summary>
        /// When the next item will expire.  Return TimeSpan.MaxValue to indicate that no items will expire.
        /// </summary>
        /// <returns></returns>
        protected override TimeSpan GetNextExpiry()
        {
            DateTime now = DateTime.UtcNow;
            var toExpire = (from k in CacheStorage.Keys where GetItem(k).Expires.HasValue && GetItem(k).Expires < now select GetItem(k).Expires.Value - now).ToList();
            if (!toExpire.Any())
            {
                return TimeSpan.MaxValue;
            }
            return toExpire.OrderBy(exp => exp).First();
        }

        private CachedItem GetItem(string key)
        {
            return base.Get(key) as CachedItem;
        }

        public long CacheLimit { get; set; }

        public long CacheSize
        {
            get
            {
                long cacheSize = 0;
                foreach (CachedItem item in CacheStorage.Values.OfType<CachedItem>())
                {
                    if (item.Data != null)
                    {
                        cacheSize += item.Data.Length * sizeof(char);
                    }
                }
                return cacheSize;
            }
        }

        private void InsertItem(string key, DateTime? expiration, object value)
        {
            base.Insert(key, new CachedItem(value, expiration), null);

            // Remove any items that make the cache size too big.
            if (CacheStorage.Count > 0 && CacheSize > CacheLimit)
            {
                lock (_rwLock)
                {
                    var byOldest = CacheStorage.OrderBy(ci => (ci.Value as CachedItem).LastAccessed);
                    foreach (var item in byOldest)
                    {
                        CacheStorage.Remove(item.Key);
                        if (CacheSize < CacheLimit)
                        {
                            break;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Called periodically to remove entries that have expired.
        /// Must be implemented if you are using the Expiry timer.
        /// </summary>
        protected override void RemoveExpiredEntries()
        {
            DateTime now = DateTime.UtcNow;
            var expired = (from k in CacheStorage.Keys where GetItem(k).Expires.HasValue && GetItem(k).Expires < now select k).ToList();
            lock (_rwLock)
            {
                foreach (var key in expired)
                {
                    CacheStorage.Remove(key);
                }
            }
        }

        /// <summary>
        /// Gets the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public override object Get(string key)
        {
            CachedItem cachedItem = GetItem(key);
            // If we don't have an expiration, or the item has not expired, return it.
            if (cachedItem != null)
            {
                if (cachedItem.Expires.HasValue && cachedItem.Expires < DateTime.UtcNow)
                {
                    // Past due.  Remove it.
                    base.Delete(key);
                    return null;
                }
                else
                {
                    cachedItem.LastAccessed = DateTime.UtcNow;
                    return cachedItem;
                }
            }
            return null;
        }

        /// <summary>
        /// Inserts the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="o">The object.</param>
        /// <param name="lifetime">The lifetime.</param>
        /// <returns></returns>
        public override bool Insert(string key, object o, TimeSpan? lifetime)
        {
            if (!String.IsNullOrEmpty(key))
            {
                lock (_rwLock)
                {
                    if (o == null)
                    {
                        CacheStorage.Remove(key);
                    }
                    else
                    {
                        if (lifetime == null)
                        {
                            InsertItem(key, null, o);
                        }
                        else
                        {
                            InsertItem(key, DateTime.UtcNow + lifetime, o);
                            UpdateExpiryTimer();
                        }
                    }
                }
                return true;
            }

            return false;
        }
    }

    /// <summary>
    /// Cache implemented as a dictionary with no expiration.
    /// </summary>
    public class SimpleDictionaryCache : CacheBase, ICache
    {
        public SimpleDictionaryCache()
            : this(null)
        {
        }

        public SimpleDictionaryCache(IDictionary<string, object> storage)
        {
            if (storage == null)
            {
                CacheStorage = new Dictionary<string, object>();
            }
            else
            {
                CacheStorage = storage;
            }
        }

        protected readonly object _rwLock = new object();
        protected IDictionary<string, object> CacheStorage;

        public virtual object Get(string key)
        {
            if (String.IsNullOrEmpty(key))
            {
                return null;
            }

            lock (_rwLock)
            {
                if (CacheStorage.ContainsKey(key))
                {
                    return CacheStorage[key];
                }
                return null;
            }
        }

        public virtual bool Insert(string key, object o, TimeSpan? lifetime)
        {
            if (String.IsNullOrEmpty(key))
            {
                return false;
            }

            lock (_rwLock)
            {
                if (o == null)
                {
                    CacheStorage.Remove(key);
                }
                else
                {
                    CacheStorage[key] = o;
                }
            }
            return true;
        }

        public void Delete(string key)
        {
            if (!String.IsNullOrEmpty(key))
            {
                lock (_rwLock)
                {
                    CacheStorage.Remove(key);
                }
            }
        }

        public void Clear()
        {
            CacheStorage.Clear();
        }
    }
}
